package cn.asiontang.db;

import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import cn.asiontang.exception.LogEx;
import cn.asiontang.launcher.BuildConfig;
import cn.asiontang.utils.ToastEx;

public class DBHelper
{
    private static class DBOpenHelper extends SQLiteOpenHelper
    {
        private static final String TAG = DBOpenHelper.class.getSimpleName();
        private final Context mContext;
        private final int mSqlCreateDBResId;
        private final int mSqlUpgradeDBResId;

        public DBOpenHelper(final Context context//
                , final String dbName//
                , final int versionNumber//
                , final int sqlCreateDBResId//
                , final int sqlUpgradeDBResId)
        {
            super(context, dbName, null, versionNumber);
            this.mContext = context;
            this.mSqlCreateDBResId = sqlCreateDBResId;
            this.mSqlUpgradeDBResId = sqlUpgradeDBResId;
        }

        /**
         * Execute all of the SQL statements in the String[] array
         *
         * @param db  The database on which to execute the statements
         * @param sql An array of SQL statements to execute
         */
        private void execMultipleSQL(final SQLiteDatabase db, final String[] sql)
        {
            db.beginTransaction();
            String tempSql = null;
            try
            {
                for (final String s : sql)
                {
                    tempSql = s;
                    if (s.trim().length() > 0)
                        db.execSQL(s);
                }
                db.setTransactionSuccessful();
            } catch (final SQLException e)
            {
                LogEx.e(TAG, tempSql, e);
            } catch (final Exception e)
            {
                LogEx.e(TAG, tempSql, e);
            } finally
            {
                db.endTransaction();
            }
        }

        /**
         * 当需要创建数据库表的时候调用。
         */
        @Override
        public void onCreate(final SQLiteDatabase db)
        {
            final String[] sqls = this.mContext.getString(this.mSqlCreateDBResId).split(";");
            this.execMultipleSQL(db, sqls);

            LogEx.i(TAG, "onCreate");
        }

        @Override
        public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion)
        {
            ToastEx.showAndWaitLong("检测高版本的数据，请安装正确的APP版本");

            super.onDowngrade(db, oldVersion, newVersion);

            LogEx.w(TAG, "onDowngrade", "oldVersion:", oldVersion, "newVersion:", newVersion);
        }

        /**
         * 当指定的数据库版本不同时，需要升级就调用。
         */
        @Override
        public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion)
        {
            if (newVersion - oldVersion > 1)
                LogEx.e("检测到跨版本升级数据库", "onUpgrade", "oldVersion", oldVersion, "newVersion", newVersion);
            else
                LogEx.w("onUpgrade", "oldVersion", oldVersion, "newVersion", newVersion);

            // 因为在创建表的SQL语句中使用了CREATE TABLE if not exists ，所以可以直接全部执行一遍，
            // 其它需要修改部分表结构的，正常使用mSqlUpgradeDBResId的SQL语句即可。
            this.onCreate(db);

            final Resources res = this.mContext.getResources();

            //根据 mSqlUpgradeDBResId 获取 每次升级都执行的SQL语句
            final String[] sqls = res.getString(this.mSqlUpgradeDBResId).split(";");
            this.execMultipleSQL(db, sqls);

            //通过 资源名反射 获取 只在特定版本升级时 执行的SQL语句
            final String resFullName = res.getResourceName(this.mSqlUpgradeDBResId);
            for (int i = oldVersion + 1; i <= newVersion; i++)
            {
                final int resId = res.getIdentifier(resFullName + "_" + i, null, null);
                if (resId == 0)
                {
                    //此种情况很少见，所以作为错误列出。
                    LogEx.e(resFullName + "_" + i, "resId=0 忽略处理");
                    continue;
                }
                if (BuildConfig.DEBUG)
                    LogEx.d(resFullName + "_" + i, res.getString(resId));

                this.execMultipleSQL(db, res.getString(resId).split(";"));
            }
            // if(DEBUG)
            // Log.e(TAG,"使用升级[方法2]：先删除全部旧表，再重新创建新表。会丢失所有老数据。");
            // [方法2]：先删除全部旧表，再重新创建新表。会丢失所有老数据。
            // onCreate(db);
        }
    }

    private static final String TAG = "DBHelper";
    private static DBOpenHelper sOpenHeler;

    private static Application sContext;

    private static final Object mLocker = new Object();

    /**
     * <pre>
     * <b>必须在finally里调用 endTransaction</b>
     * <code>     {@link #beginTransaction() DBHelper.beginTransaction(); }
     *     try{
     *         //some code
     *         {@link #setTransactionSuccessful() DBHelper.setTransactionSuccessful(); }
     *     }
     *     finally{
     *         {@link #endTransaction() DBHelper.endTransaction(); }
     *     }
     * </code>
     * </pre>
     *
     * @see android.database.sqlite.SQLiteDatabase#beginTransaction()
     */
    public static final void beginTransaction()
    {
        sOpenHeler.getWritableDatabase().beginTransaction();
    }

    /**
     * 立即关闭数据库连接。仅在APP整个退出时，调用。
     */
    public static final void closeDatabase()
    {
        if (sOpenHeler == null)
            return;
        try
        {
            sOpenHeler.close();
        } catch (final Exception ignore)
        {
            ignore.printStackTrace();
        }
    }

    /**
     */
    public static final void closeSilently(final Cursor cursor)
    {
        try
        {
            if (cursor != null)
                cursor.close();
        } catch (final Exception ignore)
        {
            //吃掉异常
            ignore.printStackTrace();
        }
    }

    public static final void endTransaction()
    {
        if (sOpenHeler.getWritableDatabase().inTransaction())
            sOpenHeler.getWritableDatabase().endTransaction();
    }

    /**
     * 自动执行String.format(sContext.getString(sqlResId), args)的写法。
     */
    public static final void execSQL(final int sqlResId, final Object... args)
    {
        execSQL(sContext.getString(sqlResId), args);
    }

    /**
     * 自动执行String.format(sql, args)的写法。
     */
    public static final void execSQL(final String sql, final Object... args)
    {
        if (BuildConfig.DEBUG)
            LogEx.d(TAG, "execSQL", String.format(sql, args));

        if (args.length > 0)
            sOpenHeler.getWritableDatabase().execSQL(String.format(sql, args));
        else
            sOpenHeler.getWritableDatabase().execSQL(sql);
    }

    /**
     * 参数化执行SQL语句
     */
    public static final void execSQLByArgs(final int sqlResId, final Object... args)
    {
        execSQLByArgs(sContext.getString(sqlResId), args);
    }

    /**
     * 参数化执行SQL语句
     */
    public static final void execSQLByArgs(final String sql, final Object... args)
    {
        if (BuildConfig.DEBUG)
        {
            LogEx.d(TAG, "execSQL", sql, args);
            try
            {
                //将参数化的东西“尝试”替换为String.format(format, args)形式，方便调试。
                LogEx.d(TAG, "execSQL - format", String.format(//
                        sql.replaceAll("\\?(\\d*)", "'%$1\\$s'").replaceAll("%\\$s", "%s")//
                        , args));
            } catch (final Exception ignore)
            {
            }
        }

        if (args.length > 0)
            sOpenHeler.getWritableDatabase().execSQL(sql, args);
        else
            sOpenHeler.getWritableDatabase().execSQL(sql);
    }

    /**
     * 检测是否需要【暴力升级(先删除整个数据库再新建数据库)】
     */
    @SuppressWarnings("unused")
    private static final void forceUpgradeDb(final Application app, final String dbName, final int versionNumber, final int sqlUpgradeDBResId)
    {
        final File dbFile = app.getDatabasePath(dbName);
        if (!dbFile.exists())
            return;

        SQLiteDatabase oldDb = null;
        try
        {
            oldDb = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READONLY);

            final int oldVersion = oldDb.getVersion();

            //检测到需要升级数据库
            if (oldVersion < versionNumber)
                //检测是否需要先删除数据库，再创建数据库
                if (isNeedDropWholeDb(oldVersion, versionNumber, sqlUpgradeDBResId))
                {
                    LogEx.w("forceUpgradeDb", "NeedDropWholeDb", "oldVersion:", oldVersion, "versionNumber:", versionNumber);
                    app.deleteDatabase(dbName);
                }
        } catch (final android.database.sqlite.SQLiteDiskIOException e)
        {
            //友盟曾经捕获到 db.getVersion 时抛出了此异常：disk I/O error (code 3850)
            //导致了升级数据库异常，无法正常使用。
            //所以尝试直接删除数据库。
            try
            {
                app.deleteDatabase(dbName);
            } catch (final Exception ex)
            {
                LogEx.e("发生了意料中的异常", e.getMessage(), "尝试直接删除数据库失败", ex);
            }
        } catch (final Exception e)
        {
            LogEx.e("暴力升级检测", e);
        } finally
        {
            try
            {
                if (oldDb != null && oldDb.isOpen())
                    oldDb.close();
            } catch (final Exception ignore)
            {
                //吃掉异常
                ignore.printStackTrace();
            }
        }
    }

    /**
     * <b style='font-size:18;'>Cursor用完必须在 finally 里关闭；调用closeSilently<br/></b>
     * 自动执行String.format(sContext.getString(sqlResId), args)的写法。
     *
     * @see {@link #getCursor(SQLiteDatabase, String)}
     */
    public static final Cursor getCursor(final int sqlResId, final Object... args)
    {
        return getCursor(sContext.getString(sqlResId), args);
    }

    /**
     * <b style='font-size:18;'>Cursor用完必须在 finally 里关闭；调用closeSilently<br/></b>
     */
    public static final Cursor getCursor(final String sql, final Object... args)
    {
        if (BuildConfig.DEBUG)
            LogEx.d(TAG, "getCursor", String.format(sql, args));

        if (args.length > 0)
            return sOpenHeler.getReadableDatabase().rawQuery(String.format(sql, args), null);

        return sOpenHeler.getReadableDatabase().rawQuery(sql, null);
    }

    /**
     * <b style='font-size:18;'>Cursor用完必须在 finally 里关闭；调用closeSilently<br/></b>
     * 查询时，使用参数化传递Value，防止SQL注入。
     */
    public static final Cursor getCursorByArgs(final int sqlResId, final String... args)
    {
        return getCursorByArgs(sContext.getString(sqlResId), args);
    }

    /**
     * <b style='font-size:18;'>Cursor用完必须在 finally 里关闭；调用closeSilently<br/></b>
     * 查询时，使用参数化传递Value，防止SQL注入。
     *
     * <pre>
     * 支持的占位符有：
     * ?
     * ?index
     * :Name
     * \@Name
     * $Name
     * </pre>
     */
    public static final Cursor getCursorByArgs(final String sql, final String[] selectionArgs)
    {
        if (BuildConfig.DEBUG)
        {
            LogEx.d(TAG, "getCursorByArgs", sql, selectionArgs);
            try
            {
                //将参数化的东西“尝试”替换为String.format(format, args)形式，方便调试。
                LogEx.d(TAG, "getCursorByArgs - format", String.format(//
                        sql.replaceAll("\\?(\\d*)", "'%$1\\$s'").replaceAll("%\\$s", "%s")//
                        , (Object[]) selectionArgs));
            } catch (final Exception ignore)
            {
            }
        }

        return sOpenHeler.getReadableDatabase().rawQuery(sql, selectionArgs);
    }

    /**
     * 一旦在init之后调用则，得到的将是“升级后”的版本号。切记！
     *
     * @see {@link SQLiteDatabase#getVersion()}
     */
    public static final int getDatabaseVersion()
    {
        return sOpenHeler.getReadableDatabase().getVersion();
    }

    /**
     * 参考实现:.NET 的 SqlCommand.ExecuteScalar 方法
     * 执行查询，并返回查询所返回的结果集中第一行的第一列。忽略其他列或行。
     */
    public static final Object getScalar(final Cursor cursor)
    {
        try
        {
            if (!cursor.moveToNext())
                return null;
            return cursor.getString(0);
            //=======================================================================================
            // 不再使用getType来调用对应的get函数来获取强类型数据，防止数据库存的数据本身出问题时会导致崩溃
            // 直接取出文本任由调用者自己转换！
            //=======================================================================================
            //	    if (android.os.Build.VERSION.SDK_INT < 11)
            //		return cursor.getString(0);
            //	    //getType 需要API 等级为 11才能调用.
            //	    switch (cursor.getType(0))
            //	    {
            //		case Cursor.FIELD_TYPE_INTEGER:
            //		    return cursor.getInt(0);
            //		case Cursor.FIELD_TYPE_FLOAT:
            //		    return cursor.getFloat(0);
            //		case Cursor.FIELD_TYPE_STRING:
            //		    return cursor.getString(0);
            //		case Cursor.FIELD_TYPE_BLOB:
            //		    return cursor.getBlob(0);
            //		default:
            //		    return null;
            //	    }
        } finally
        {
            closeSilently(cursor);
        }
    }

    /**
     * 参考实现:.NET 的 SqlCommand.ExecuteScalar 方法
     * 执行查询，并返回查询所返回的结果集中第一行的第一列。忽略其他列或行。
     */
    public static final Object getScalar(final int sqlResId, final Object... args)
    {
        return getScalar(getCursor(sqlResId, args));
    }

    public static final Object getScalar(final String sql, final Object... args)
    {
        return getScalar(getCursor(sql, args));
    }

    public static final Object getScalarByArgs(final int sqlResId, final String... args)
    {
        return getScalar(getCursorByArgs(sqlResId, args));
    }

    public static final Object getScalarByArgs(final String sql, final String... args)
    {
        return getScalar(getCursorByArgs(sql, args));
    }

    /**
     * 获得一个索引0的列表
     */
    public static final List<String> getStringList(final Cursor cursor)
    {
        try
        {
            final List<String> lst = new ArrayList<String>(cursor.getCount());
            while (cursor.moveToNext())
                lst.add(cursor.getString(0));
            return lst;
        } finally
        {
            DBHelper.closeSilently(cursor);
        }
    }

    /**
     * 获得一个将索引0做为KEY，索引1作为Value来构建的LinkedHashMap
     */
    public static final Map<String, String> getStringMap(final Cursor cursor)
    {
        try
        {
            final Map<String, String> map = new LinkedHashMap<String, String>(cursor.getCount());
            while (cursor.moveToNext())
                map.put(cursor.getString(0), cursor.getString(1));
            return map;
        } finally
        {
            DBHelper.closeSilently(cursor);
        }
    }

    /**
     * @param app               context to use to open or create the database
     * @param dbName            name of the database file, or null for an in-memory database
     * @param versionNumber     version number of the database (starting at 1); if the
     *                          database is older, onUpgrade will be used to upgrade the
     *                          database; if the database is newer, onDowngrade will be used
     *                          to downgrade the database
     * @param sqlCreateDBResId  用来创建数据库的SQL语句的资源ID（使用;符号分割每张表的SQL语句）
     * @param sqlUpgradeDBResId 用来升级老数据库的SQL语句的资源ID（使用;符号分割每张表的SQL语句）
     */
    public static final void init(final Application app, final String dbName, final int versionNumber, final int sqlCreateDBResId,
                                  final int sqlUpgradeDBResId)
    {
        //=====================================================================================
        // 因为init在Application里面调用，而Application可能存在被多次启动的情况，而且是不同进程空间，
        // 此时 forceUpgradeDb 操作的却是相同文件，就会出现文件被锁的情况。因此此处得处理一下。
        //=====================================================================================
        synchronized (mLocker)
        {
            if (sContext != null)
                return;

            sContext = app;

            //================================================================================
            // 鉴于有客户反应数据库存在无缘无故被清空的问题，将数据库操作相关的危险代码暂时禁用。
            //================================================================================
            //检测是否需要【暴力升级(先删除整个数据库再新建数据库)】
            // forceUpgradeDb(app, dbName, versionNumber, sqlUpgradeDBResId);

            sOpenHeler = new DBOpenHelper(app, dbName, versionNumber, sqlCreateDBResId, sqlUpgradeDBResId);
        }
    }

    public static final long insertOrReplace(final String tableName, final ContentValues contentvalues)
    {
        return sOpenHeler.getWritableDatabase().insertWithOnConflict(tableName, null, contentvalues, SQLiteDatabase.CONFLICT_REPLACE);
    }

    /**
     * 自带事务，不需要手动开启事务。
     */
    public static final void insertOrReplaceMultiRows(final String tableName, final ArrayList<ContentValues> arrayValues)
    {
        DBHelper.beginTransaction();
        try
        {
            for (final ContentValues c : arrayValues)
            {
                if (c == null)
                    continue;
                final long rowId = DBHelper.insertOrReplace(tableName, c);
                if (rowId == -1)
                    throw new Exception("出现插入冲突(Conflict)！返回值rowId为-1。");
            }
            DBHelper.setTransactionSuccessful();
        } catch (final Exception ex)
        {
            LogEx.e(TAG, tableName, ex);
        } finally
        {
            DBHelper.endTransaction();
        }
    }

    /**
     * 自带事务，不需要手动开启事务。
     */
    public static final void insertOrReplaceMultiRows(final String tableName, final ContentValues... arrayValues)
    {
        DBHelper.beginTransaction();
        try
        {
            for (final ContentValues c : arrayValues)
            {
                if (c == null)
                    continue;
                final long rowId = DBHelper.insertOrReplace(tableName, c);
                if (rowId == -1)
                    throw new Exception("出现插入冲突(Conflict)！返回值rowId为-1。");
            }
            DBHelper.setTransactionSuccessful();
        } catch (final Exception ex)
        {
            LogEx.e(TAG, tableName, ex);
        } finally
        {
            DBHelper.endTransaction();
        }
    }

    private static boolean isNeedDropWholeDb(final int oldVersion, final int newVersion, final int mSqlUpgradeDBResId)
    {
        final Resources res = sContext.getResources();
        //通过 资源名反射 获取 只在特定版本升级时 执行的SQL语句
        final String resFullName = res.getResourceName(mSqlUpgradeDBResId);
        for (int i = oldVersion + 1; i <= newVersion; i++)
        {
            //例如：sql_UpgradeDB_2_drop_db，表示升级到版本2数据库时，需要删除整个数据库（暴力升级谨慎使用！）。
            final String newResIdName = String.format("%1$s_%2$s_drop_db", resFullName, i);
            final int resId = res.getIdentifier(newResIdName, null, null);
            if (resId == 0)
                continue;
            //当存在跨多个版本升级时，只要有任意一个版本是需要“全删整个数据库”来完成升级的，则此次直接删除再升级即可。
            return true;
        }
        return false;
    }

    public static final void setTransactionSuccessful()
    {
        sOpenHeler.getWritableDatabase().setTransactionSuccessful();
    }
}
